package org.axed.user.client;

import java.util.ArrayList;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Element;

import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;

/**
 * Every AxedArea has one and exactly one Document, which holds
 * the lines and handles all edits to them.
 */
public class Document implements AxedListener {
	/**
	 * Possible endLine parameters for find/match
	 */
	public static final int OPERATE_EOD  = -1;
	public static final int OPERATE_WRAP = -2;

	/**
	 * The AxedArea this document belongs to.
	 */
	AxedArea axed;

	/**
	 * The div the document is child of.
	 */
	Element parentDiv;

	/**
	 * A list of all lines.
	 */
	ArrayList lines = new ArrayList();

	/**
	 * The current font style.
	 */
	int font = AxedArea.FONT_MONOSPACE;

	/**
	 * true when wrapping
	 */
	boolean wrap = true;

	/**
	 * Current font size.
	 */
	public String fontSize = "12px";

	/**
	 * The line height in pixels.
	 */
	int lineHeight = 10;
	
	/**
 	 * The column width in pixels.
 	 */ 
	int colWidth = 10;

	/**
	 * Size of the document in bytes.
	 */
	int size = 0;

	/**	
	 * Constructor.
	 */
	public Document(AxedArea axed, Element parentDiv) {
		this.axed = axed;
		this.parentDiv = parentDiv;

		Line l = new Line(this);
		lines.add(l);
		DOM.appendChild(parentDiv, l.getCurses().getElement());
	}


	/**
	 * Finds the next occurance of text after startLine and startCol.
	 * Results will be placed in fr.
	 *
	 * @return true if found
	 */
	public boolean find(String text, 
	                    int startLine, int startCol, 
	                    int endLine, int endCol, 
						FindResult fr) {
		int es = endLine >= 0 ? endLine : lines.size();
		int io;
		StringBuffer strResult = new StringBuffer();

		for(int i = startLine; i < es; i++) {
			Line l = getLine(i);
			if (i == startLine && i == endLine) {
				io = l.findStr(text, startCol, endCol);
			} else if (i == startLine) {
				io = l.findStr(text, startCol, l.size());
			} else if (i == endLine) {
				io = l.findStr(text, 0, endCol);
			} else {
				io = l.findStr(text);
			}
			if (io >= 0) {
				fr.wrapped = false;
				fr.line = i;
				fr.col = io;
				return true;
			}
		}

		if (endLine != OPERATE_WRAP) {
			return false;
		}

		for (int i = 0; i <= startLine; i++) {
			Line l = getLine(i);
			if (i == startLine && i == endLine) {
				io = l.findStr(text, startCol, endCol);
			} else if (i == startLine) {
				io = l.findStr(text, startCol, l.size());
			} else if (i == endLine) {
				io = l.findStr(text, 0, endCol);
			} else {
				io = l.findStr(text);
			}
			if (io >= 0) {
				fr.wrapped = true;
				fr.line = i;
				fr.col = io;
				return true;
			}
		}
		return false;
	}

	/**
	 * Matches the next regular experssion after startLine and startCol.
	 * Results will be placed in fr.
	 *
	 * @return true if found
	 */
	public boolean match(String expr, 
	                     int startLine, int startCol, 
	                     int endLine, int endCol, 
					     FindResult fr,
						 boolean matchStart) {
		int es = endLine >= 0 ? endLine : lines.size();
		int io;
		StringBuffer strResult = new StringBuffer();
		if (!matchStart) {
			startCol++;
		}

		for(int i = startLine; i < es; i++) {
			Line l = getLine(i);
			if (i == startLine && i == endLine) {
				if (startCol >= l.size()) {
					continue;
				}
				io = l.findStr(expr, startCol, endCol);
			} else if (i == startLine) {
				if (startCol >= l.size()) {
					continue;
				}
				io = l.findStr(expr, startCol, l.size());
			} else if (i == endLine) {
				io = l.findStr(expr, 0, endCol);
			} else {
				io = l.findStr(expr);
			}
			if (io >= 0) {
				fr.wrapped = false;
				fr.line = i;
				fr.col = io;
				return true;
			}
		}

		if (endLine != OPERATE_WRAP) {
			return false;
		}

		for (int i = 0; i <= startLine; i++) {
			Line l = getLine(i);
			if (i == startLine && i == endLine) {
				io = l.findStr(expr, startCol, endCol);
			} else if (i == startLine) {
				io = l.findStr(expr, startCol, l.size());
			} else if (i == endLine) {
				io = l.findStr(expr, 0, endCol);
			} else {
				io = l.findStr(expr);
			}
			if (io >= 0) {
				fr.wrapped = true;
				fr.line = i;
				fr.col = io;
				return true;
			}
		}
		return false;
	}

	/**
	 * Return the character at line/col.
	 */
	public char getChar(int line, int col) {
		return getLine(line).text().charAt(col);
	}

	/**
	 * Returns the column width.
	 * This only makes sense when using monospaced font.
	 */
	public int getColWidth() {
		return colWidth;
	}

	/**
	 * Returns the edit width.
	 */
	public int getEditWidth() {
		return axed.getWidth();
	}

	/**
	 * Returns the font as specified by the FONT_* enumeration
	 */
	public int getFont() {
		return font;
	}

	/**
	 * Returns line i.
	 */
	public Line getLine(int i) {
		assert i < lines.size();
		return (Line) lines.get(i);
	}

	/** 
	 * Returns the total number of lines.
	 */
	public int getLineCount() {
		return lines.size();
	}

	/**
	 * Returns the line height.
	 */
	public int getLineHeight() {
		return lineHeight;
	}

	/**
	 * Returns the line-number of a Line object.
	 */
	public int getLinePos(Line l) {
		return lines.indexOf(l);
	}

	/**
	 * Returns the documents contents.
	 */
	public String getText() {
		String text = "";
		int lsize = lines.size();
		for(int i = 0; i < lsize; i++) {
			text += (i > 0 ? "\n" : "") + getLine(i).text();
		}
		return text;
	}

	/**
	 * Returns the text from line1/col1 to line2/col2
	 */
	public String getText(int line1, int line2) {
		StringBuffer s = new StringBuffer();
		if (line2 >= lines.size()) {
			line2 = lines.size() - 1;
		}

		s.append(getLine(line1).text());
		for (int i = line1 + 1; i < line2; i++) {
			s.append('\n');
			s.append(getLine(i).text());
		}

		s.append('\n');
		return s.toString();
	}

	/**
	 * returns true when wrapping.
	 */
	public boolean getWrap() {
		return wrap;
	}

	/**
	 * Returns the text from line1/col1 to line2/col2
	 */
	public String getText(int line1, int col1, int line2, int col2) {
		StringBuffer s = new StringBuffer();

		if (line1 == line2) {
			// its one-lined
			return getLine(line1).substring(col1, col2);
		}

		s.append(getLine(line1).substring(col1, getLine(line1).size()));

		for (int i = line1 + 1; i < line2; i++) {
			s.append('\n');
			s.append(getLine(i).text());
		}

		s.append('\n');
		s.append(getLine(line2).substring(0, col2));
		return s.toString();
	}

	/**
	 * Inserts a line. 
	 * 
	 * @param l			the line to insert
	 * @param linepos	linepos
	 */
	public void insertLine(Line l, int linepos) {
		if (linepos + 1 == lines.size()) {
			lines.add(l);
			DOM.appendChild(parentDiv, l.getCurses().getElement());
		} else  {
			Line lineAfter = getLine(linepos + 1);
			lines.add(linepos + 1, l);
			DOM.insertBefore(parentDiv, l.getCurses().getElement(), 
			                 lineAfter.getCurses().getElement());
		}
	}
	
	/**
	 * Inserts the text into the document.
	 * 
	 * @param text
	 */
	private void insertText(AxedEvent.PntText ev) {
		String text = ev.text;
		if(ev.text.indexOf('\t') != -1) {
			text = ev.text.replaceAll("\t", "  ");
		}
		String[] plines = text.split("\n", -1);
		if (plines.length == 0) {
			return;
		}

		// make the first line
		String s1 = plines[0];
		getLine(ev.iv.p1.line).insert(s1, ev.iv.p1.col); // TODO make more elegent call
		size += s1.length();
		if (plines.length == 1) {
			ev.iv.p2.set(ev.iv.p1.line, ev.iv.p1.col + s1.length());
			return;
		}
		splitLine(ev.iv.p1.line, ev.iv.p1.col + s1.length()); //TODO, elegeance
		size++;
		for (int l = 1; l + 1 < plines.length; l++) {
			String s = plines[l];
			Line nl = new Line(this, s);
			insertLine(nl, ev.iv.p1.line + l - 1);
			nl.setFont(font); //TODO why is it needed
			nl.setWrap(wrap);
			size += s.length() + 1;
		}
		int elnr = ev.iv.p1.line + plines.length - 1; // TODO, elegance
		String s2 = plines[plines.length - 1];
		Line l2 = getLine(elnr);
		l2.insert(s2, 0);
		ev.iv.p2.set(elnr, s2.length());
	}

	/**
	 * Joins a line with its follower
	 */
	private void joinLine(AxedEvent.Pointer ev) {
		Line lb = getLine(ev.pnt.line);
		Line la = getLine(ev.pnt.line + 1);
		int cb = lb.size();

		lb.join(la);

		removeLine(la);
		size--;
	}

	/**
	 * Splits a line and inserts a new one with the remaining text.
	 */
	private void splitLine(int line, int col) {
		Line nl = getLine(line).split(col);
		insertLine(nl, line);
		nl.setWrap(wrap);
		size++;
	}

	
	/**
	 * Adds a character.
	 */
	private void insertChar(AxedEvent.PntChar ev) {
		Line l = getLine(ev.pnt.line);
		int lh = l.getCurses().getHeight();
		size++;
		l.insert(ev.ch, ev.pnt.col);
		if (lh != l.getCurses().getHeight()) {
			axed.lineHeightChanged(ev.pnt.line);
		}
	}

	/**
	 * Handles events from the ace area.
	 */
	public void onAxedEvent(AxedEvent event) {
		switch (event.type) {
		case AxedEvent.REMOVE_CHAR :
			removeChar(event.getRemoveChar());
			break;
		case AxedEvent.FONT_SIZE_CHANGED :
			fontSizeChanged(event.getFontChange().testcase);
			break;
		case AxedEvent.INSERT_CHAR :
			insertChar(event.getInsertChar());
			break;
		case AxedEvent.INSERT_TEXT :
			insertText(event.getInsertText());
			break;
		case AxedEvent.JOIN_LINE :
			joinLine(event.getJoinLine());
			break;
		case AxedEvent.REMOVE_TEXT :
			removeText(event.getRemoveText());
			break;
		case AxedEvent.REPLACE_EXPR :
			replaceExpr(event.getReplaceExpr());
			break;
		case AxedEvent.SET_FONT :
			setFont(event.getSetFont().font);
			break;
		case AxedEvent.SPLIT_LINE :
			{
				AxedPointer p = event.getSplitLine().pnt;
				splitLine(p.line, p.col);
			}
			break;
		}
	}


	/**
	 * Handles a replace expr event
	 */
	private void replaceExpr(AxedEvent.ReplaceExpr ev) {
		int c = 0;
		for (int i = ev.iv.p1.line; i <= ev.iv.p2.line; i++) {
			// TODO column restrictions are not taken care of!
			Line l = getLine(i);
			String s = l.text();
			String r;
			if (ev.global) {
				r = s.replaceAll(ev.expr, ev.replacement);
			} else {
				r = s.replaceFirst(ev.expr, ev.replacement);
			}
 			if (!r.equals(s)) {
				c += ev.global ? (s.split(ev.expr).length - 1) : 1;
				axed.removeText(ev.common().atomicID, i, 0, l.size());
				axed.insertText(ev.common().atomicID, i, 0, r, null);
			}
		}
		ev.replaceNum = c;
	}

	/**
	 * Sets the font of the widget.
	 *
	 * @param font A int value of the FONT_* enumaration.
	 */
	public void setFont(int font) {
		if (this.font == font) {
			return;
		}

		this.font = font;
		for(int i = 0; i < lines.size(); i++) {
			getLine(i).setFont(font);
		}
	}

	/**
	 * Sets wrapping true or false
	 */
	public void setWrap(boolean wrap) {
		this.wrap = wrap;
		for (int i = 0; i < lines.size(); i++) {
			getLine(i).setWrap(wrap);
		}
		axed.refresh();
	}

	/**
	 * Returns the size of the document in bytes
	 * @return size
	 */
	public int size() {
		return size;
	}
	
//privates
	/**
	 * TODO
	 */
	private void fontSizeChanged(Element testCase) {
		switch (font) {
		case AxedArea.FONT_MONOSPACE:
			DOM.setStyleAttribute(testCase, "fontFamily", "monospace");
			break;
		case AxedArea.FONT_SERIF:
			DOM.setStyleAttribute(testCase, "fontFamily", "serif");
			break;
		}
		DOM.setStyleAttribute(testCase, "fontSize", fontSize);
				 
		lineHeight = DOM.getElementPropertyInt(testCase, "offsetHeight");
		colWidth = DOM.getElementPropertyInt(testCase, "offsetWidth");
	}

	/**
	 * Removes a character.
	 */
	private void removeChar(AxedEvent.PntChar ev) {
		Line l = getLine(ev.pnt.line);
		int lh = l.getCurses().getHeight();
		ev.ch = l.remove(ev.pnt.col);
		size--;
		if (lh != l.getCurses().getHeight()) {
			axed.lineHeightChanged(ev.pnt.line);
		}
	}

	/**
	 * Removes a line.
	 *
	 * @param l    The Line to be removed.
	 */
	private void removeLine(Line l) {
		lines.remove(l);
		Element sp = l.getCurses().getElement();
		DOM.removeChild(parentDiv, sp);
	}

	/**
	 * Removes a portion from the document.
	 *
	 * TODO elegance
	 */
	private void removeText(AxedEvent.IntervalText ev) {
		AxedInterval iv = ev.iv;
		Line beginLine = getLine(iv.p1.line);
		Line endLine = getLine(iv.p2.line);
		int bc = iv.p1.col;
		int ec = iv.p2.col;
		boolean doswitch = false;

		if (iv.isOneLined()) {
			ev.text = beginLine.remove(bc, ec);
			size -= ec - bc;
			return;
		} 

		StringBuffer rmText = new StringBuffer();
		// otherwise its more than one line.
		// trim starting line
		size -= beginLine.size() - bc;
		rmText.append(beginLine.remove(bc, beginLine.size()));
		rmText.append('\n');
		// delete the lines in the middle
		int bln = iv.p1.line + 1;      // bln .. begin line num
		int eln;
		while ((eln = endLine.getLineNum()) > bln) {
			Line rl = getLine(bln);
			rmText.append(rl.text());
			rmText.append('\n');
			size -= rl.size() + 1;
			removeLine(rl);
		}

		size -= ec + 1;
		rmText.append(endLine.remove(0, ec));
		beginLine.join(endLine);
		removeLine(endLine);
		ev.text = rmText.toString();
	}
}
